CQRS and Mediator Design Patterns in .Net 6
Introduction
CQRS
CQRS stands for Command and Query Responsibility Segregation, a design pattern that separates read and update operations for a data store. Implementing CQRS in your application can maximize its performance, scalability, and security. The flexibility created by migrating to CQRS allows a system to better evolve over time and prevents update commands from causing merge conflicts at the domain level. I’ve posted an article explaining how a CQRS pattern can be used to scale the MySQL database horizontally
Mediator
Mediator design pattern is one of the important and widely used behavioral design patterns. Mediator enables decoupling of objects by introducing a layer in between so that the interaction between objects happens via the layer. If the objects interact with each other directly, the system components are tightly-coupled with each other that makes higher maintainability cost and not hard to extend. Mediator pattern focuses on providing a mediator between objects for communication and help in implementing loose-coupling between objects.
Problem
In traditional architecture, we use the same database for query and update operations. That is simple and works well for basic CRUD operations. In more complex applications, however, this approach can become unmanageable. Then you start refactoring your code and try to separate read and update calls probably by implementing the CQRS pattern. In the DotNet world developers use one service to manage everything related to one specific entity, but if you implement CQRS pattern then you need multiple services for queries and commands and if you inject all these dependencies using Dependency Injection, then there will be lot of services in a simple Controller.
Solution
Mediator pattern can help you resolve the above problem. One mediator class or library can call the required commands and query services based on the input models. So you just need to inject one Interface and that interface will manage further dependencies. We use Dependency Injection to make our application loosely coupled, but the Mediator pattern will make this further de-coupled and simplified.
Package
The MediatR Nuget package can be used to implement the Mediator pattern in .Net. You can use the below commands to install the required packages
dotnet add package MediatR dotnet add package MediatR.Extensions.Microsoft.DependencyInjection
We will be using Dapper
Micro ORM
in this application to do the database operations. I’ll be explaining about the Dapper
in a separate article. Install Dapper
by running the below command
dotnet add package Dapper
MediatR mainly uses two interface to implement the Mediator pattern
- IRequest<T>
- IRequestHandler<T, U>
More details can be found here
How
I’m going to define a folder structure and naming convention as follows but feel free to use your own conventions.
- Commands For all commands(CUD Operations), these are
POCO
classes implementsIRequest<T>
- CommandHandlers All the business logic to execute the commands
- Queries Same as commands but only for Read operations
- QueryHandlers All the business logic to execute the queries
I’ve created two DbContexts
called ToDoContextRead
and ToDoContextWrite
both pointing to the same database, but in a Production scenario you can use separate database connection strings for both DbContexts
. More on that topic is mentioned here.
ToDoContextRead
will look like the below
public class ToDoContextRead{ private readonly string _connectionString; public ToDoContextRead(IConfiguration configuration) { _connectionString = configuration.GetConnectionString("SqlConnectionRead"); } public IDbConnection CreateConnection() => new SqlConnection(_connectionString);}
I’ve also created two Repositories for reading and writing to the databases
ToDoRepositoryRead
will look like the below:
public class ToDoRepositoryRead : IToDoRepositoryRead{ private readonly ToDoContextRead _context; public ToDoRepositoryRead(ToDoContextRead context) { _context = context; } public async Task<ToDo> GetToDoById(Guid id) { var query = "SELECT * FROM ToDos where id=@id"; var param = new { id }; using var connection = _context.CreateConnection(); var todo = await connection.QueryFirstOrDefaultAsync<ToDo>(query, param); return todo; } public async Task<IEnumerable<ToDo>> GetToDos() { var query = "SELECT * FROM ToDos"; using var connection = _context.CreateConnection(); var todos = await connection.QueryAsync<ToDo>(query); return todos.ToList(); }}
ToDoRepositoryWrite
code is as follows:
public class ToDoRepositoryWrite : IToDoRepositoryWrite{ private readonly ToDoContextWrite _context; public ToDoRepositoryWrite(ToDoContextWrite context) { _context = context; } public async Task<int> DeleteToDoById(Guid id) { var query = "DELETE FROM ToDos where id=@id"; var param = new { id }; using var connection = _context.CreateConnection(); return await connection.ExecuteAsync(query, param); } public async Task<ToDo> GetToDoById(Guid id) { var query = "SELECT * FROM ToDos where id=@id"; var param = new { id }; using var connection = _context.CreateConnection(); var todo = await connection.QueryFirstOrDefaultAsync<ToDo>(query, param); return todo; } public async Task<IEnumerable<ToDo>> GetToDos() { var query = "SELECT * FROM ToDos"; using var connection = _context.CreateConnection(); var todos = await connection.QueryAsync<ToDo>(query); return todos.ToList(); } public async Task<int> SaveToDo(ToDo toDo) { var query = @"INSERT INTO ToDos (Id, Title, Description, Created, IsCompleted) VALUES (@Id, @Title, @Description, @Created, @IsCompleted);"; toDo.Created = DateTime.Now; using var connection = _context.CreateConnection(); return await connection.ExecuteAsync(query, toDo); } public async Task<int> UpdateToDo(ToDo toDo) { var query = @"UPDATE ToDos SET Title=@Title, Description=@Description, Created=@Created, IsCompleted=@IsCompleted WHERE Id=@Id"; toDo.Created = DateTime.Now; using var connection = _context.CreateConnection(); return await connection.ExecuteAsync(query, toDo); }}
Commands and Queries
Now the important step is to create the Commands
and Queries
. Commands and Queries are simple DTOs
or POCO
classes, but in order to work with the MediatR
library, we need to implement an Interface
called IRequest<T>
. A sample Command is shown below. All other commands and queries can be found in the Github Repo.
public class CreateToDoCommand : IRequest<ToDo>{ public string? Title { get; set; } public string? Description { get; set; } public CreateToDoCommand(string? title, string? description) { Title = title; Description = description; } public CreateToDoCommand() { }}
In the above example, the ToDo
class in the IRequest
Interface is the return type. For each command or query there should be a Handler
defined. Here is the Handler
for CreateToDoCommand
:
public class CreateToDoCommandHandler : IRequestHandler<CreateToDoCommand, ToDo>{ private readonly IToDoRepositoryWrite _toDoRepositoryWrite; public CreateToDoCommandHandler(IToDoRepositoryWrite toDoRepositoryWrite) { _toDoRepositoryWrite = toDoRepositoryWrite; } public async Task<ToDo> Handle(CreateToDoCommand request, CancellationToken cancellationToken) { var todo = new ToDo { Created = DateTime.Now, Description = request.Description, Title = request.Title, Id = Guid.NewGuid(), IsCompleted = false }; var result = await _toDoRepositoryWrite.SaveToDo(todo); if (result > 0) { return todo; } else { throw new ArgumentException("Unable to save the ToDo"); } }}
Similar way you can define all your commands, queries and handlers. Once that part is ready then you need to configure the Mediator service in the Program.cs
file as follows:
builder.Services.AddMediatR(typeof(ToDoContextRead).GetTypeInfo().Assembly);
In the above line ToDoContextRead
is used just for getting the assembly, and this line will do all the magic to binding the commands and queries to the handlers.
Now you can inject the Mediator
into your controller as follows:
private readonly IMediator _mediator;public ToDosController(IMediator mediator){ _mediator = mediator;}
Now you can call any handlers by simply sending the commands as follows
var todos = await _mediator.Send(new GetToDoDetailQuery { Id = id });
Complete code sample can be found at https://github.com/kannan-kiwitech/CqrsMediatorSampleApi.
Happy coding!